Simulation plans ================= A **simulation plan** is the object that collects every choice you might want to impose on a forward path -- a conditioning value on an endogenous variable at a given date, a pinned or activated shock, whether a constraint is anticipated or hits as a surprise, an algebraic identity that must hold over a window -- into a single object the toolbox's simulators consume. The plan is the **universal interface** for controlled simulations in RISE: * ``simulate`` drives the perturbation solution forward through the plan. * ``forecast`` produces conditional forecasts (with or without shock uncertainty) from the plan. * ``perfect_foresight`` solves the full nonlinear model under the plan as a deterministic boundary-value problem. The same plan flows into all three; what changes between them is the *solver*, not the plan. This chapter is the reference for building the plan. The next chapter, :doc:`Forecasting and simulation`, is the reference for the solvers that consume it. .. contents:: :local: :depth: 2 Why a simulation plan ---------------------- A plan separates *what you want enforced* from *how the solver enforces it*. Two payoffs: 1. **One vocabulary for stochastic and deterministic contexts.** A DSGE solved by perturbation accepts anticipated shocks natively, so a plan that announces a shock :math:`k` periods ahead -- or springs it as a surprise at :math:`t` -- works identically through ``simulate`` and through ``perfect_foresight``. You do not switch APIs to switch contexts. 2. **Hard endogenous constraints with an explicit identification audit.** A plan that pins :math:`y_t` at a chosen value requires a free shock at :math:`t` to enforce it. Because the plan stores the shock matrix explicitly -- pinned entries are numeric, free entries are ``NaN`` -- the identification condition is checkable at construction time, before the solver is ever called. Plans also accept algebraic constraints (a linear combination of variables fixed over a window), bounds (soft inequality constraints, where the corresponding feature is supported), and multi-page anticipation structures (the same shock at the same date entered with several lead times). Construction ------------- The constructor takes the model, a horizon, an anticipation horizon, and (optionally) a shock-initialization rule:: sp = simplan(m, [start_date, end_date], anticipationHorizon, ... 'shockInit', initRule); The four arguments: * ``m`` -- a solved (or at least parsed) ``rise_model`` object, used to read the endogenous, exogenous and parameter lists and to anchor dates. * ``[start_date, end_date]`` -- the horizon. ``start_date`` is the *last date of history* (the point from which the simulation continues forward); ``end_date`` is the *last simulation date*. Dates may be integers (``[1, 40]``) or RISE date objects (``[rq(2020,1), rq(2030,4)]``). * ``anticipationHorizon`` -- the maximum number of periods ahead a shock can be announced. ``1`` means contemporaneous only; ``k`` allows announcements up to :math:`k-1` periods ahead. * ``'shockInit', initRule`` -- the default value for every shock, at every date, before any ``append`` calls. See below. The ``shockInit`` rule ~~~~~~~~~~~~~~~~~~~~~~~ ``shockInit`` chooses what every shock entry starts as: .. list-table:: :header-rows: 1 :widths: 25 75 * - Value - Effect * - ``'zero'`` (default) - Every shock pinned at ``0``. ``append`` lifts entries individually -- either to a numeric value (still pinned) or to ``NaN`` (handed to the solver). * - ``'randn'`` - Every shock drawn from a standard normal. Reproduce by seeding the RNG before construction. * - numeric scalar - Every shock pinned at the scalar. * - struct - Per-shock rule, where each field name is a shock and the value is any of the rules above. Example:: spec = struct(); spec.EZ = 'randn'; spec.ER = 'zero'; spec.EETA = 'zero'; rng(42) sp = simplan(m, [rq(2020,1), rq(2030,4)], 1, 'shockInit', spec); ``shockInit`` only sets defaults; later ``append`` calls overwrite the entries they touch. Appending conditions --------------------- ``append`` is the workhorse. Three forms cover almost everything. Single variable, vector of values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pin one variable over a window:: target_dates = rq(2022,1):rq(2022,4); sp = simplan(m, [rq(2020,1), rq(2030,4)], 1, 'shockInit', 'zero'); sp = append(sp, 'PAI', target_dates, 1.005 * ones(4,1)); Numeric values pin the variable. A single scalar is replicated across the date range:: sp = append(sp, 'R', rq(2023,1):rq(2023,4), 1.01); Multiple variables, struct form ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When several variables share the same dates:: S = struct(); S.PAI = 1.005 * ones(4,1); S.R = 1.010 * ones(4,1); sp = append(sp, S, rq(2022,1):rq(2022,4)); Scalar fields in the struct are replicated across the dates. Algebraic constraint ~~~~~~~~~~~~~~~~~~~~~ A linear combination of variables can be fixed over a window through a constraint-string and a fixing shock:: sp = append(sp, ... {'R{t} - 1.005 * PAI{t} = 0', 'ER'}, ... rq(2022,1):rq(2022,4), ... NaN); The constraint string uses the same syntax as model equations (``var{t}`` for the contemporaneous variable). The fixing shock (``ER`` here) is the shock the solver is allowed to use to enforce the constraint -- it must be activated (``NaN``) at the same dates. Boundary conditions: initval, endval, histval ---------------------------------------------- Besides ``append``, a plan exposes Dynare-style setters for the boundary conditions of a perfect-foresight problem. They are convenient when porting a Dynare model, or when you think in terms of *initial* / *terminal* / *historical* blocks rather than per-date pins. ``initval(plan, steady, C)`` Set the **initial** conditions (and, in the absence of other blocks, the start values and the terminal condition). ``steady`` is a logical: ``true`` treats ``C`` as a guess and solves the steady state from it; ``false`` uses the values as given. ``C`` is the conditions -- ``{name, value}`` pairs, a cell of ``'name=value;'`` strings, or a single string:: plan = initval(plan, false, {'k', 12; 'c', 1.1}); plan = initval(plan, false, 'k=12; c=1.1;'); ``endval(plan, steady, C)`` Set the **terminal** conditions, with the same ``steady`` flag and the same ``C`` formats. Use it for a permanent-change experiment whose terminal steady state differs from the initial one:: plan = endval(plan, true, {'tau', 0.25}); % solve the terminal SS at tau = 0.25 ``histval(plan, C)`` Set the **historical** (lagged) initial conditions, with the lag written into the name:: plan = histval(plan, {'k(-1)', 12; 'y(0)', 1}); ``from_history(m, db, history_end_date, nsteps)`` Build a plan whose initial conditions are **read from a historical database** at the jump-off date. For every declared endogenous variable present in ``db``, the jump-off value and its lags are written into the plan; the simulation then projects ``nsteps`` periods forward:: plan = simplan.from_history(m, db, rq(2021,4), 40); These setters and ``append`` combine freely: a common pattern is ``histval`` (or ``from_history``) for the lagged initial state plus ``append`` for known future shocks. For commitment / optimal-policy models, whether the initial *policy multipliers* start at the steady state or at zero is a separate choice -- see ``simul_optimal_policy_start`` in :doc:`../ModelShapes/DSGE/Deterministic and quasi-deterministic solutions`. Anticipation: pages -------------------- Each shock entry sits on a *page* indexed by lead time: * Page ``1`` -- the shock is realised at the date but unanticipated before then. * Page ``k`` (for ``k <= anticipationHorizon``) -- the shock is *announced* at the date but realised :math:`k-1` periods later; agents see it that many periods in advance. Pages let surprises and announcements coexist in the same plan. An anticipated monetary shock at ``rq(2023,1)`` announced two quarters ahead:: sp = append(sp, 'ER', rq(2023,1), 0.01, 3); (page ``3`` because the announcement is :math:`k-1 = 2` periods ahead). The same shock and date entered on page ``1`` would be an unanticipated realization. The two coexist if you write both. Sequences of announcements with different lead times are common:: shock_dates = rq(2024,1):rq(2024,4); shock_values = [0.02; 0.015; 0.01; 0.005]; for lead = 0:3 sp = append(sp, 'EZ', shock_dates(lead+1), ... shock_values(lead+1), lead+1); end Each entry is the same shock at a different date, announced a different number of periods ahead. Hard endogenous conditions and the identification rule ------------------------------------------------------- Pinning a variable at a chosen value is a *hard endogenous condition*. The solver enforces it by choosing values for *free* shocks at the same dates. Two ingredients are required: 1. The variable is appended with a numeric value at the condition dates. 2. Enough shocks are *activated* at the condition dates by being set to ``NaN``. At every date :math:`t` in the conditioning window: .. math:: \#\{\text{NaN shock entries at } t\} \;\geq\; \#\{\text{endogenous constraints at } t\}. If the inequality fails, the solver raises an identification error (``active constraints exceed free shocks``). The recipe ~~~~~~~~~~~ A non-trivial conditional forecast follows four steps: 1. Construct the plan with ``shockInit = 'zero'``. 2. Append background shocks at fixed values (these stay pinned and shape the baseline scenario). 3. **Activate** the shocks the solver may use, by appending them at ``NaN`` at the conditioning dates. 4. Append the endogenous conditions (numeric values at those same dates). A worked single-variable example -- a faster disinflation than the Taylor rule alone would deliver, after a cost-push shock:: horizon = [rq(2020,1), rq(2028,1)]; cond_dates = rq(2021,1):rq(2022,4); % 8-quarter window sp = simplan(m, horizon, 1, 'shockInit', 'zero'); % Step 2: background -- cost-push shock at the start of history sp = append(sp, 'EETA', rq(2020,2), 2.0); % Step 3: activate the monetary shock at the conditioning dates sp = append(sp, 'ER', cond_dates, NaN); % Step 4: pin inflation to the target path PAI_target = linspace(PAI_uncon_peak, PAI_ss, numel(cond_dates)).'; sp = append(sp, 'PAI', cond_dates, PAI_target); At each date in ``cond_dates`` there is exactly one constraint (``PAI``) and exactly one ``NaN`` shock (``ER``): the system is *exactly identified* and the solver backs out the ``ER`` path that makes inflation follow ``PAI_target``. Two-variable conditioning is the same pattern with two activated shocks:: sp = simplan(m, horizon, 1, 'shockInit', 'zero'); sp = append(sp, 'EETA', rq(2020,2), 2.0); % background sp = append(sp, 'ER', cond_dates, NaN); % activate sp = append(sp, 'EETA', cond_dates, NaN); % activate sp = append(sp, 'PAI', cond_dates, PAI_target); sp = append(sp, 'R', cond_dates, R_peg); Two constraints, two free shocks -- exactly identified. .. note:: Appending ``EETA = NaN`` at ``cond_dates`` overrides the zero that ``shockInit`` placed there. The earlier background entry ``EETA = 2.0`` at ``rq(2020,2)`` is untouched -- only the ``cond_dates`` entries are overwritten. Soft conditions ~~~~~~~~~~~~~~~~ A **soft condition** restricts a variable to a *band* instead of pinning it to a single value. You append a two-element ``[lo hi]`` value where a hard condition would take a scalar:: sp = append(sp, {'Y', cond_dates, [-0.5 0.5]}); % a band sp = append(sp, {'R', cond_dates, [ 0.0 NaN]}); % one-sided (a floor) A scalar is a hard point; a ``[lo hi]`` pair is a band. An open side is written ``NaN`` -- ``[lo NaN]`` is a floor and ``[NaN hi]`` a ceiling. The plan recognizes a band automatically:: is_soft(sp) % true when the plan carries at least one (non-degenerate) band A degenerate band ``[v v]`` is just the hard point ``v`` -- the hard condition is the zero-width limit of the soft one. A band is still an *endogenous* condition, so the identification rule above applies unchanged: at least as many shocks must be freed (set to ``NaN``) at each conditioning date as there are conditioned variables. The convenience method ``free_shocks`` does the freeing in bulk:: sp = free_shocks(sp, 'EPS_D', cond_dates); % free one shock sp = free_shocks(sp, 'all', cond_dates); % free every shock -- % recovers the classic % minimum-norm conditioning *How* a band is consumed -- a simulated distribution (a fan chart) or a single conditional mean -- is chosen on the solver call through the ``simul_soft_conditioning`` option, documented in :doc:`Forecasting and simulation`. A plan that carries no band is an ordinary hard-conditioning plan and is solved exactly as described above. Inspecting a plan ------------------ Three methods help you check what is in a plan: * ``disp(sp)`` -- a formatted summary: the horizon, the number of constrained endogenous and exogenous entries, the pages in use. * ``details(sp)`` -- the raw property dump, useful when something in ``disp`` does not match what you expected. * ``query(sp, name, dates, pages)`` -- inspect specific entries. ``query`` examples:: query(sp, 'PAI') % all conditioned PAI dates query(sp, 'PAI', rq(2022,2)) % a specific date query(sp, 'ER', rq(2023,1)) % a shock across all pages query(sp, 'ER', rq(2023,1), 3) % a specific page ``query`` is the right tool to verify the identification rule before calling the solver -- count the ``NaN`` entries at each condition date and compare to the number of pinned endogenous constraints. Consumption: the universal interface ------------------------------------- Once a plan is built, the same plan drives every consumer. The plan is passed through the ``simul_historical_data`` option:: res_sim = simulate (m, simul_historical_data = sp); res_fkst = forecast (m, simul_historical_data = sp, ... forecast_nsteps = H); res_pf = perfect_foresight(m, simul_historical_data = sp); The three solvers differ in what they assume about the model solution and the shock structure, not in how they read the plan: .. list-table:: :header-rows: 1 :widths: 25 30 45 * - Solver - Solution it uses - When to prefer it * - ``simulate`` - The perturbation solution (linear or higher-order) returned by ``solve``. - Fast. Accurate near steady state. The default for stochastic conditional simulations. * - ``forecast`` - Same perturbation solution, with options for shock uncertainty (``forecast_shock_uncertainty``) and the hard/soft-condition routing. - When you want a true *forecast* -- shocks drawn past the conditioning window, fan charts, the conditional-forecast output structure. * - ``perfect_foresight`` - The full nonlinear model solved as a boundary-value problem over the entire horizon. - More accurate for large or persistent deviations; honours occasionally-binding constraints exactly; for deterministic scenarios. The identification rule is solver-independent: a plan that is under-identified for one solver is under-identified for all three. This is by design -- the plan separates the *problem statement* from the *solution method*. Scope: shapes other than DSGE ------------------------------ The simulation-plan object is built around the canonical fields of the ``rise_model`` class and therefore works for every factory that returns one. The construction interface (horizons, pages, ``append`` patterns, the identification rule) is identical across shapes. Each shape's chapter under :doc:`Model shapes <../ModelShapes/DSGE/index>` documents the consumer-side options that are specific to that shape (e.g. structural-shock identification under SVAR conditioning). See also --------- * :doc:`Forecasting and simulation` -- the consumer side: ``simulate``, ``forecast``, ``perfect_foresight``, their option surfaces, fan charts, relative-entropy tilting. * :doc:`Filtering` -- the smoother-based alternative for *linear* models with conditions on *observed* variables (no plan required; based on least squares). * :doc:`../ModelShapes/DSGE/Deterministic and quasi-deterministic solutions` -- the deterministic context in full; uses plans throughout. * :doc:`../ModelShapes/DSGE/Occasionally-binding constraints` -- scenarios that combine the plan with OBC routes.